package com.bolt.common.xml;

import com.bolt.common.io.FileUtil;
import com.bolt.common.io.IoUtil;
import com.bolt.common.lang.Assert;
import com.bolt.common.utils.CharsetUtil;
import com.bolt.common.utils.StrUtil;
import com.bolt.convention.exception.ServiceException;
import org.apache.commons.lang3.StringEscapeUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.beans.XMLEncoder;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by Administrator on 2019/11/27.
 */
public class XmlUtil {
    /**
     * 在XML中无效的字符 正则
     */
    public static final String INVALID_REGEX = "[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]";
    /**
     * XML格式化输出默认缩进量
     */
    public static final int INDENT_DEFAULT = 2;
    /**
     * 默认的DocumentBuilderFactory实现
     */
    private static String defaultDocumentBuilderFactory = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl";

    /**
     * 读取解析XML文件
     */
    public static XPathParser readXML(File file) {
        Assert.notNull(file, "Xml file is null !");
        if (false == file.exists()) {
            throw new ServiceException("File [{}] not a exist!", file.getAbsolutePath());
        }
        if (false == file.isFile()) {
            throw new ServiceException("[{}] not a file!", file.getAbsolutePath());
        }

        try {
            file = file.getCanonicalFile();
        } catch (IOException e) {
            // ignore
        }

        BufferedInputStream in = null;
        try {
            in = FileUtil.getInputStream(file);
            return readXML(in);
        } finally {
            IoUtil.close(in);
        }
    }


    /**
     * 读取解析XML文件<br>
     * 编码在XML中定义
     */
    public static XPathParser readXML(InputStream inputStream) throws ServiceException {
        return readXML(new InputSource(inputStream));
    }

    /**
     * 读取解析XML文件
     */
    public static XPathParser readXML(Reader reader) throws ServiceException {
        return readXML(new InputSource(reader));
    }


    /**
     * 将String类型的XML转换为XML文档
     */
    public static XPathParser readXML(String xmlStr) {
        if (StrUtil.isBlank(xmlStr)) {
            throw new IllegalArgumentException("XML content string is empty !");
        }
        xmlStr = cleanInvalid(xmlStr);
        return readXML(StrUtil.getReader(xmlStr));
    }

    /**
     * 读取解析XML文件
     */
    public static XPathParser readXML(InputSource source) throws ServiceException {
        final XPathParser xPathParser = new XPathParser(source);
        return xPathParser;
    }

    /**
     * 去除XML文本中的无效字符
     */
    public static String cleanInvalid(String xmlContent) {
        if (xmlContent == null) {
            return null;
        }
        return xmlContent.replaceAll(INVALID_REGEX, "");
    }

    /**
     * 将XML文档转换为String<br>
     * 字符编码使用XML文档中的编码，获取不到则使用UTF-8<br>
     * 默认非格式化输出，若想格式化请使用{@link #format(Document)}
     *
     * @param doc XML文档
     * @return XML字符串
     */
    public static String toStr(Document doc) {
        return toStr(doc, false);
    }

    /**
     * 将XML文档转换为String<br>
     * 字符编码使用XML文档中的编码，获取不到则使用UTF-8
     *
     * @param doc      XML文档
     * @param isPretty 是否格式化输出
     * @return XML字符串
     */
    public static String toStr(Document doc, boolean isPretty) {
        return toStr(doc, CharsetUtil.UTF_8, isPretty);
    }

    /**
     * 将XML文档转换为String<br>
     * 字符编码使用XML文档中的编码，获取不到则使用UTF-8
     *
     * @param doc      XML文档
     * @param charset  编码
     * @param isPretty 是否格式化输出
     * @return XML字符串
     * @since 3.0.9
     */
    public static String toStr(Document doc, String charset, boolean isPretty) {
        final StringWriter writer = StrUtil.getWriter();
        try {
            write(doc, writer, charset, isPretty ? INDENT_DEFAULT : 0);
        } catch (Exception e) {
            throw new ServiceException("Trans xml document to string error!", e);
        }
        return writer.toString();
    }

    /**
     * 格式化XML输出
     *
     * @param doc {@link Document} XML文档
     * @return 格式化后的XML字符串
     */
    public static String format(Document doc) {
        return toStr(doc, true);
    }

    /**
     * 格式化XML输出
     *
     * @param xmlStr XML字符串
     * @return 格式化后的XML字符串
     */
    public static String format(String xmlStr) {
        XPathParser xPathParser = readXML(xmlStr);
        return format(xPathParser.getDocument());
    }

    /**
     * 将XML文档写入到文件<br>
     * 使用Document中的编码
     *
     * @param doc          XML文档
     * @param absolutePath 文件绝对路径，不存在会自动创建
     */
    public static void toFile(Document doc, String absolutePath) {
        toFile(doc, absolutePath, null);
    }

    /**
     * 将XML文档写入到文件<br>
     *
     * @param doc     XML文档
     * @param path    文件路径绝对路径或相对ClassPath路径，不存在会自动创建
     * @param charset 自定义XML文件的编码，如果为{@code null} 读取XML文档中的编码，否则默认UTF-8
     */
    public static void toFile(Document doc, String path, String charset) {
        if (StrUtil.isBlank(charset)) {
            charset = doc.getXmlEncoding();
        }
        if (StrUtil.isBlank(charset)) {
            charset = CharsetUtil.UTF_8;
        }

        BufferedWriter writer = null;
        try {
            writer = FileUtil.getWriter(path, charset, false);
            write(doc, writer, charset, INDENT_DEFAULT);
        } finally {
            IoUtil.close(writer);
        }
    }

    /**
     * 将XML文档写出
     *
     * @param node    {@link Node} XML文档节点或文档本身
     * @param writer  写出的Writer，Writer决定了输出XML的编码
     * @param charset 编码
     * @param indent  格式化输出中缩进量，小于1表示不格式化输出
     */
    public static void write(Node node, Writer writer, String charset, int indent) {
        transform(new DOMSource(node), new StreamResult(writer), charset, indent);
    }

    /**
     * 将XML文档写出
     *
     * @param node    {@link Node} XML文档节点或文档本身
     * @param out     写出的Writer，Writer决定了输出XML的编码
     * @param charset 编码
     * @param indent  格式化输出中缩进量，小于1表示不格式化输出
     */
    public static void write(Node node, OutputStream out, String charset, int indent) {
        transform(new DOMSource(node), new StreamResult(out), charset, indent);
    }

    /**
     * 将XML文档写出<br>
     * 格式化输出逻辑参考：https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java
     *
     * @param source  源
     * @param result  目标
     * @param charset 编码
     * @param indent  格式化输出中缩进量，小于1表示不格式化输出
     */
    public static void transform(Source source, Result result, String charset, int indent) {
        final TransformerFactory factory = TransformerFactory.newInstance();
        try {
            final Transformer xformer = factory.newTransformer();
            if (indent > 0) {
                xformer.setOutputProperty(OutputKeys.INDENT, "yes");
                xformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent));
            }
            if (StrUtil.isNotBlank(charset)) {
                xformer.setOutputProperty(OutputKeys.ENCODING, charset);
            }
            xformer.transform(source, result);
        } catch (Exception e) {
            throw new ServiceException("Trans xml document to string error!", e);
        }
    }

    // -------------------------------------------------------------------------------------- Create

    /**
     * 创建XML文档<br>
     * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码
     *
     * @return XML文档
     */
    public static Document createXml() {
        return createDocumentBuilder().newDocument();
    }

    /**
     * 创建 DocumentBuilder
     *
     * @return DocumentBuilder
     */
    public static DocumentBuilder createDocumentBuilder() {
        DocumentBuilder builder;
        try {
            builder = createDocumentBuilderFactory().newDocumentBuilder();
        } catch (Exception e) {
            throw new ServiceException("Create xml document error!", e);
        }
        return builder;
    }

    /**
     * 创建{@link DocumentBuilderFactory}
     *
     * @return {@link DocumentBuilderFactory}
     */
    public static DocumentBuilderFactory createDocumentBuilderFactory() {
        final DocumentBuilderFactory factory;
        if (StrUtil.isNotEmpty(defaultDocumentBuilderFactory)) {
            factory = DocumentBuilderFactory.newInstance(defaultDocumentBuilderFactory, null);
        } else {
            factory = DocumentBuilderFactory.newInstance();
        }
        return disableXXE(factory);
    }

    /**
     * 创建XML文档<br>
     * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码
     *
     * @param rootElementName 根节点名称
     * @return XML文档
     */
    public static Document createXml(String rootElementName) {
        return createXml(rootElementName, null);
    }

    /**
     * 创建XML文档<br>
     * 创建的XML默认是utf8编码，修改编码的过程是在toStr和toFile方法里，即XML在转为文本的时候才定义编码
     *
     * @param rootElementName 根节点名称
     * @param namespace       命名空间，无则传null
     * @return XML文档
     * @since 5.0.4
     */
    public static Document createXml(String rootElementName, String namespace) {
        final Document doc = createXml();
        doc.appendChild(null == namespace ? doc.createElement(rootElementName) : doc.createElementNS(rootElementName, namespace));
        return doc;
    }

    // -------------------------------------------------------------------------------------- Function

    /**
     * 获得XML文档根节点
     *
     * @param doc {@link Document}
     * @return 根节点
     * @see Document#getDocumentElement()
     * @since 3.0.8
     */
    public static Element getRootElement(Document doc) {
        return (null == doc) ? null : doc.getDocumentElement();
    }


    /**
     * 根据节点名获得子节点列表
     *
     * @param element 节点
     * @param tagName 节点名，如果节点名为空（null或blank），返回所有子节点
     * @return 节点列表
     */
    public static List<Element> getElements(Element element, String tagName) {
        final NodeList nodeList = StrUtil.isBlank(tagName) ? element.getChildNodes() : element.getElementsByTagName(tagName);
        return transElements(element, nodeList);
    }

    /**
     * 根据节点名获得第一个子节点
     *
     * @param element 节点
     * @param tagName 节点名
     * @return 节点
     */
    public static Element getElement(Element element, String tagName) {
        final NodeList nodeList = element.getElementsByTagName(tagName);
        if (nodeList == null || nodeList.getLength() < 1) {
            return null;
        }
        int length = nodeList.getLength();
        for (int i = 0; i < length; i++) {
            Element childEle = (Element) nodeList.item(i);
            if (childEle == null || childEle.getParentNode() == element) {
                return childEle;
            }
        }
        return null;
    }

    /**
     * 根据节点名获得第一个子节点
     *
     * @param element 节点
     * @param tagName 节点名
     * @return 节点中的值
     */
    public static String elementText(Element element, String tagName) {
        Element child = getElement(element, tagName);
        return child == null ? null : child.getTextContent();
    }

    /**
     * 根据节点名获得第一个子节点
     *
     * @param element      节点
     * @param tagName      节点名
     * @param defaultValue 默认值
     * @return 节点中的值
     */
    public static String elementText(Element element, String tagName, String defaultValue) {
        Element child = getElement(element, tagName);
        return child == null ? defaultValue : child.getTextContent();
    }

    /**
     * 将NodeList转换为Element列表
     *
     * @param nodeList NodeList
     * @return Element列表
     */
    public static List<Element> transElements(NodeList nodeList) {
        return transElements(null, nodeList);
    }

    /**
     * 将NodeList转换为Element列表<br>
     * 非Element节点将被忽略
     *
     * @param parentEle 父节点，如果指定将返回此节点的所有直接子节点，null返回所有就节点
     * @param nodeList  NodeList
     * @return Element列表
     */
    public static List<Element> transElements(Element parentEle, NodeList nodeList) {
        int length = nodeList.getLength();
        final ArrayList<Element> elements = new ArrayList<>(length);
        Node node;
        Element element;
        for (int i = 0; i < length; i++) {
            node = nodeList.item(i);
            if (Node.ELEMENT_NODE == node.getNodeType()) {
                element = (Element) nodeList.item(i);
                if (parentEle == null || element.getParentNode() == parentEle) {
                    elements.add(element);
                }
            }
        }

        return elements;
    }

    /**
     * 将可序列化的对象转换为XML写入文件，已经存在的文件将被覆盖<br>
     */
    public static void writeObjectAsXml(File dest, Object bean) {
        XMLEncoder xmlenc = null;
        try {
            xmlenc = new XMLEncoder(FileUtil.getOutputStream(dest));
            xmlenc.writeObject(bean);
        } finally {
            // 关闭XMLEncoder会相应关闭OutputStream
            IoUtil.close(xmlenc);
        }
    }

    /**
     * 关闭XXE，避免漏洞攻击<br>
     *
     * @param dbf DocumentBuilderFactory
     * @return DocumentBuilderFactory
     */
    private static DocumentBuilderFactory disableXXE(DocumentBuilderFactory dbf) {
        String feature;
        try {
            // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
            // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
            feature = "http://apache.org/xml/features/disallow-doctype-decl";
            dbf.setFeature(feature, true);
            // If you can't completely disable DTDs, then at least do the following:
            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
            // JDK7+ - http://xml.org/sax/features/external-general-entities
            feature = "http://xml.org/sax/features/external-general-entities";
            dbf.setFeature(feature, false);
            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
            // JDK7+ - http://xml.org/sax/features/external-parameter-entities
            feature = "http://xml.org/sax/features/external-parameter-entities";
            dbf.setFeature(feature, false);
            // Disable external DTDs as well
            feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
            dbf.setFeature(feature, false);
            // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);
        } catch (ParserConfigurationException e) {
            // ignore
        }
        return dbf;
    }





}

 